﻿namespace ClassyBlog.Infrastructure
{
    using System;
    using System.Diagnostics;
    using System.IO;
    using System.Net;
    using System.Text;

    public class Http : IHttp
    {
        private const int BufferLength = 8096;

        private static readonly Func<Uri, WebRequest> defaultFactory = url =>
        {
            var request = (HttpWebRequest)WebRequest.Create(url);

            request.Method = "GET";
            request.AllowAutoRedirect = true;
            request.MaximumAutomaticRedirections = 2;
            request.Timeout = 5 * 1000; // Is 5 Seconds too much?
            request.AutomaticDecompression = DecompressionMethods.GZip |
                                             DecompressionMethods.Deflate;
            request.Accept = "*.*";
            request.Expect = string.Empty;
            request.CookieContainer = new CookieContainer();

            return request;
        };

        private static Func<Uri, WebRequest> factory;

        public static Func<Uri, WebRequest> CreateRequest
        {
            [DebuggerStepThrough]
            get { return factory ?? defaultFactory; }

            [DebuggerStepThrough]
            set { factory = value; }
        }

        public virtual void GetAsync(
            Uri url, Action<bool, int, string, string> onComplete)
        {
            if (url == null)
            {
                throw new ArgumentNullException("url");
            }

            if (onComplete == null)
            {
                throw new ArgumentNullException("onComplete");
            }

            var request = CreateRequest(url);

            var state = new ReadState
                            {
                                OnComplete = onComplete,
                                Request = request
                            };

            WrapCritical<InvalidOperationException>(
                state,
                () => request.BeginGetResponse(
                    OnBeginGetResponse, state));
        }

        private static void OnBeginGetResponse(IAsyncResult result)
        {
            var state = (ReadState)result.AsyncState;
            WebResponse response = null;

            if (!WrapCritical<WebException>(
                state, () => response = state.Request.EndGetResponse(result)))
            {
                return;
            }

            var statusCode = 200;
            var httpResponse = response as HttpWebResponse;

            if (httpResponse != null)
            {
                statusCode = (int)httpResponse.StatusCode;
            }

            if (IsError(statusCode))
            {
                ThrowErrorOnComplete(state.OnComplete, statusCode);
                return;
            }

            Stream stream = null;

            if (!WrapCritical<ProtocolViolationException>(
                state, () => stream = response.GetResponseStream()))
            {
                return;
            }

            state.StatusCode = statusCode;
            state.ContentType = response.ContentType;
            state.OrignalStream = stream;
            state.BufferStream = new MemoryStream();
            state.Data = new byte[BufferLength];

            WrapCritical<IOException>(
                state,
                () => stream.BeginRead(
                    state.Data, 0, BufferLength, OnBeginReadComplete, state));
        }

        private static void OnBeginReadComplete(IAsyncResult result)
        {
            var state = (ReadState)result.AsyncState;

            var bytesRead = 0;

            if (!WrapCritical<IOException>(
                state, () => bytesRead = state.OrignalStream.EndRead(result)))
            {
                return;
            }

            if (bytesRead > 0)
            {
                state.BufferStream.Write(state.Data, 0, bytesRead);

                WrapCritical<IOException>(
                    state,
                    () => state.OrignalStream.BeginRead(
                        state.Data,
                        0,
                        BufferLength,
                        OnBeginReadComplete,
                        state));

                return;
            }

            state.BufferStream.Flush();

            var content = Encoding.UTF8.GetString(
                state.BufferStream.ToArray());

            state.CloseStreams();

            state.OnComplete(
                false, state.StatusCode, state.ContentType, content);
        }

        private static bool WrapCritical<TException>(
            ReadState state, Action block) where TException : Exception
        {
            try
            {
                block();
                return true;
            }
            catch (TException)
            {
                state.CloseStreams();
                ThrowErrorOnComplete(state.OnComplete);
            }

            return false;
        }

        private static void ThrowErrorOnComplete(
            Action<bool, int, string, string> onComplete, int statusCode = 500)
        {
            onComplete(true, statusCode, null, null);
        }

        private static bool IsError(int statusCode)
        {
            return (statusCode < (int)HttpStatusCode.OK) &&
                   (statusCode >= (int)HttpStatusCode.Ambiguous);
        }

        private sealed class ReadState
        {
            public Action<bool, int, string, string> OnComplete { get; set; }

            public WebRequest Request { get; set; }

            public int StatusCode { get; set; }

            public string ContentType { get; set; }

            public Stream OrignalStream { get; set; }

            public MemoryStream BufferStream { get; set; }

            public byte[] Data { get; set; }

            public void CloseStreams()
            {
                if (OrignalStream != null)
                {
                    OrignalStream.Close();
                }

                if (BufferStream != null)
                {
                    BufferStream.Close();
                }
            }
        }
    }
}